Skip to content

Feature: add partial pattern matching (partial=True) for callbacks#3765

Open
i-murray wants to merge 4 commits intoplotly:devfrom
i-murray:feature/partial-pattern-matching
Open

Feature: add partial pattern matching (partial=True) for callbacks#3765
i-murray wants to merge 4 commits intoplotly:devfrom
i-murray:feature/partial-pattern-matching

Conversation

@i-murray
Copy link
Copy Markdown
Contributor

@i-murray i-murray commented May 7, 2026

Feature: Partial Pattern Matching (partial=True) for Callbacks

Description

Dash's pattern-matching callbacks currently require the pattern and component IDs to have exactly the same set of keys. This means a pattern like {"type": "btn"} will not match a component with ID {"type": "btn", "index": 1} — even though the component clearly has the key and value the pattern cares about.

This PR adds an opt-in partial=True flag on Input, Output, and State that relaxes the key-matching constraint: a pattern's keys only need to be a subset of the component's keys. Extra keys on the component are ignored.

Closes #3764

Usage

@app.callback(
    Output("output", "children"),
    Input({"type": "btn"}, "n_clicks", partial=True),
    prevent_initial_call=True,
)
def on_btn_click(n_clicks):
    return f"clicked {n_clicks}"

This fires for any component whose ID contains "type": "btn", regardless of other keys:

  • {"type": "btn", "index": 1}
  • {"type": "btn", "page": "home", "section": "main"}
  • {"type": "btn", "index": 2, "tab": "first"}

Works with wildcards:

Input({"type": ALL}, "n_clicks", partial=True)    # collects all components with a "type" key
Input({"type": MATCH}, "n_clicks", partial=True)   # matches one-at-a-time on "type"

Design choices

  • Per-dependency opt-in — not a new wildcard type, not a per-callback flag. Each Input/Output/State independently opts in, so partial and non-partial deps can coexist in the same callback.
  • Extra keys are ignored — component keys not present in the pattern are treated as if they matched. This is the most permissive and intuitive behavior.
  • Literal-only partial patterns are implicitly multi-valued — a pattern like {"type": "btn"} with partial=True can match multiple components, so the callback receives a list (same as ALL). Patterns with MATCH still resolve one-at-a-time.
  • Zero-cost when unused — a hasPartialPatterns boolean guard on the callback graph ensures apps without partial=True pay no additional overhead. All new loops are skipped entirely.

Contributor Checklist

  • I have broken down my PR scope into the following TODO tasks
    • Add partial attribute to DashDependency, Input, Output, State (Python)
    • Update _id_matches() for subset key matching when partial=True
    • Serialize partial flag via to_dict() and propagate through insert_callback()
    • Add addPartialPattern() and partialIdMatch() to frontend (dependencies.js)
    • Update computeGraphs() to detect partial flags and set hasPartialPatterns
    • Update getWatchedKeys(), getCallbackByOutput(), getUnfilteredLayoutCallbacks() with partial pattern loops
    • Add resolvePartialDeps() and update getCallbacksByInput() in dependencies_ts.ts
    • Update isMultiValued() for implicit multi-valued partial patterns
    • Fix requestedCallbacks.ts type compatibility for updated isMultiValued signature
    • Add hasPartialPatterns guard to all new loops for zero-cost when unused
  • I have run the tests locally and they passed. (refer to testing section in contributing)
  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR
    • 16 unit tests in tests/unit/test_partial_matching.py — subset matching, wildcards, serialization, bidirectional checks, negative cases
    • 6 integration tests in tests/integration/callbacks/test_partial_wildcards.py — basic partial match, ALL collection, literal filtering, mixed key sets

Optionals

  • I have added entry in the CHANGELOG.md
  • If this PR needs a follow-up in dash docs, community thread, I have mentioned the relevant URLs as follows
    • this GitHub #PR number updates the dash docs
    • here is the show and tell thread in Plotly Dash community

Performance

Partial matching requires cross-keyStr searches in several hot-path functions that were previously $O(1)$ hash lookups:

Function Original With partial patterns
getWatchedKeys() $O(1)$ $O(K \cdot p)$
getCallbacksByInput() $O(1)$ $O(K \cdot p)$
getCallbackByOutput() $O(1)$ $O(K \cdot p)$
getUnfilteredLayoutCallbacks() $O(N)$ $O(N \cdot K \cdot p)$

Where $K$ = number of distinct keyStr entries in the pattern index, $p$ = number of keys in the pattern (typically 1–5), and $N$ = number of components in a layout chunk.

hasPartialPatterns guard

To ensure zero cost for apps that don't use partial=True, computeGraphs() sets a hasPartialPatterns boolean on the graph in $O(D)$ time (where $D$ = total dependencies). Every new loop is guarded:

if (graphs.hasPartialPatterns) {
    for (const patKeyStr in graphs.inputPatterns) { ... }
}
Scenario Per-event cost
No partial patterns (common case) $O(1)$ — single boolean check, same as before
With partial patterns $O(K \cdot p)$ — unavoidable cross-keyStr search

Allow pattern-matching callbacks to match components whose IDs contain a
superset of the pattern's keys. Opt-in per dependency via partial=True on
Input, Output, and State.

- Python: partial flag on DashDependency, subset-aware _id_matches()
- JS/TS: addPartialPattern, partialIdMatch, resolvePartialDeps,
  updated getWatchedKeys, getCallbacksByInput, getCallbackByOutput,
  getUnfilteredLayoutCallbacks, resolveDeps, isMultiValued
- Literal-only partial patterns are implicitly multi-valued
- hasPartialPatterns guard for zero-cost when unused
- 16 unit tests + 4 integration tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Partial Pattern Matching

1 participant